package rps.client;

import static rps.network.NetworkUtil.hostNetworkGame;

import java.rmi.RemoteException;
import java.util.ArrayList;

import rps.game.Game;
import rps.game.GameImpl;
import rps.game.data.Figure;
import rps.game.data.FigureKind;
import rps.game.data.Move;
import rps.game.data.Player;
import rps.network.GameRegistry;
import rps.network.NetworkUtil;

/**
 * This class is responsible for controlling all game related events.
 */
public class GameController implements GameListener, UItoGCInterface {

	private UIController uiController;

	private GameRegistry registry;
	private Player player;
	private Game game;
	
	/** Stores the desired assignment of the user before starting the game. */
	private FigureKind[] preAssignment = new FigureKind[GameImpl.ROWS * GameImpl.COLS];
	/**
	 * Stores the players initial choice to be able to send it later, after
	 * {@code this.provideInitialChoice()} has been called.
	 */
	private FigureKind initialChoice;
	/** Stores if an initial choice has already been committed. */
	private boolean initialChoiceCommitted = false;
	/** Stores if the game is still open */
	private boolean gameRunning = false;

	
	/*
	 * PREPARATIONAL FUNCTION CALLS
	 */
	
	public void setComponents(UIController uiController) {
		this.uiController = uiController;
	}

	public void startHostedGame(Player player, String host) {
		this.player = player;
		updateUIControllerPlayer(player);
		registry = hostNetworkGame(host);
		register(player, this);
	}

	public void startJoinedGame(Player player, String host) {
		this.player = player;
		updateUIControllerPlayer(player);
		registry = NetworkUtil.requestRegistry(host);
		register(player, this);
	}

	public void startAIGame(Player player, GameListener ai) {
		this.player = player;
		updateUIControllerPlayer(player);
		registry = NetworkUtil.hostLocalGame();
		register(new Player(ai.toString()), ai);
		register(player, this);
	}
	
	private void updateUIControllerPlayer(Player p){
		uiController.setPlayer(p);
	}

	private void register(Player player, GameListener listener) {
		try {
			GameListener multiThreadedListener = decorateListener(listener);
			registry.register(multiThreadedListener);
		} catch (RemoteException e) {
			throw new RuntimeException(e);
		}
	}

	private static GameListener decorateListener(GameListener listener) {
		try {
			listener = new MultiThreadedGameListener(listener);
			listener = new RMIGameListener(listener);
			return listener;
		} catch (RemoteException e) {
			throw new RuntimeException(e);
		}
	}

	@Override
	public void unregister() {
		try {
			if (registry != null && gameRunning) {
				registry.unregister(player);
			}
		} catch (RemoteException e) {
			throw new RuntimeException(e);
		}
	}

	/*
	 * OUTGOING MESSAGES
	 */
	
	/**
	 * Is invoked when the player sets his initial choice as a reaction to
	 * {@code provideInitialChoice}.
	 * Will call {@code game.setInitialAssignment()} first on first try and
	 * will wait for {@code this.provideInitialChoice}.
	 * If returning from an initial choice draw, will directly invoke
	 * {@code game.setInitialChoice()}.
	 * @param fk
	 */
	public void initialChoice(FigureKind fk){
		initialChoice = fk;
		
		if(initialChoiceCommitted){
			try {
				game.setInitialChoice(player, initialChoice);
			} catch (RemoteException e) {
				e.printStackTrace();
			}
		}
		else{
			checkAssignment(preAssignment);
			try {
				game.setInitialAssignment(player, preAssignment);
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
	}
	
	// initialAssignment is submitted within initialChoice().
	
	/**
	 * Moves a figure from the position {@code fromIndex} to the position {@code toIndex}.
	 * @param fromIndex
	 * @param toIndex
	 */
	public void move(int fromIndex, int toIndex){
		try {
			game.move(player, fromIndex, toIndex);
		} catch (RemoteException e) {
			throw new RuntimeException(e);
		}
	}
	
	/**
	 * Called to provide a new {@code FigureKind} after a fight between two figures
	 * with the same kind.
	 * @param kind
	 */
	public void drawChoice(FigureKind kind){
		if(kind == null)
			throw new RuntimeException("juhu");
		try {
			game.setUpdatedKindAfterDraw(player, kind);
		} catch (RemoteException e) {
			throw new RuntimeException(e);
		}
	}
	
	@Override
	public void surrender() {
		try {
			game.surrender(player);
		} catch (RemoteException e) {
			throw new RuntimeException(e);
		}
	}

	@Override
	public void resetForNewGame() {
		registry = null;
		player = null;
		game = null;
		initialChoiceCommitted = false;
		gameRunning = true;
		preAssignment = new FigureKind[GameImpl.ROWS * GameImpl.COLS];
	}

	/**
	 * Called to exit the game.
	 */
	public void exit() {
		if (registry != null) {
			unregister();
		}
		if (game != null && gameRunning) {
			surrender();
		}
	}

	@Override
	public Player getPlayer() throws RemoteException {
		return player;
	}
	
	/*
	 * INCOMING MESSAGES
	 */
	
	@Override
	public void chatMessage(Player sender, String message) throws RemoteException {
		uiController.receivedMessage(sender, message);
	}

	@Override
	public void provideInitialAssignment(Game game) throws RemoteException {
		this.game = game;
		uiController.switchToGamePane();
		uiController.startGame(game, game.getOpponent(player));
	}

	@Override
	public void provideInitialChoice() throws RemoteException {
		// If there was a submit already, this must be the request for a draw.
		if(initialChoiceCommitted){
			uiController.initialChoiceDraw();
		}
		else{
			// Else we can directly send the previously saved initial choice.
			/* ALSO AN OUTGOING MESSAGE */
			game.setInitialChoice(player, initialChoice);
			initialChoice = null;
			initialChoiceCommitted = true;
		}
	}

	/**
	 * Is called when the game initially starts.
	 */
	@Override
	public void startGame() throws RemoteException {
		uiController.updateBoard(game.getField());
		uiController.gameStarted();
		gameRunning = true;
	}

	/**
	 * Is called when it's this player's turn.
	 */
	@Override
	public void provideNextMove() throws RemoteException {
		uiController.provideNextMove();
	}

	@Override
	public void figureMoved() throws RemoteException {
		Move lastMove = game.getLastMove();
		
		int fromIndex = lastMove.getFrom();
		int toIndex = lastMove.getTo();
		
		uiController.figureMoved(fromIndex, toIndex, game.getField());
	}

	@Override
	public void figureAttacked() throws RemoteException {
		Move lastMove = game.getLastMove();
		
		int fromIndex = lastMove.getFrom();
		int toIndex = lastMove.getTo();
		Figure attackingFigure = lastMove.getOldField()[lastMove.getFrom()];
		Figure defendingFigure = lastMove.getOldField()[lastMove.getTo()];
		
		uiController.figureAttacked(fromIndex, toIndex, attackingFigure, defendingFigure, game.getField());
	}

	@Override
	public void provideChoiceAfterFightIsDrawn() throws RemoteException {
		uiController.updateBoard(game.getField());
		uiController.provideChoiceAfterFightIsDrawn();
	}

	@Override
	public void gameIsLost() throws RemoteException {
		gameRunning = false;
		if(game != null){
			uiController.updateBoard(game.getField());
		}
		uiController.gameIsLost();
	}

	@Override
	public void gameIsWon() throws RemoteException {
		gameRunning = false;
		if(game != null){
			uiController.updateBoard(game.getField());
		}
		uiController.gameIsWon();
	}

	@Override
	public void gameIsDrawn() throws RemoteException {
		gameRunning = false;
		if(game != null){
			uiController.updateBoard(game.getField());
		}
		uiController.gameIsDrawn();
	}
	
	/*
	 * INTERNAL EVENTS
	 */
	
	/**
	 * When the user changes the initial assignment this function is called to
	 * update the board and save the assignment.
	 * @param preAssignment The new initial assignment containing 14 FigureKinds.
	 */
	public void setAssignment(FigureKind[] theAssignment){
		Figure[] fieldForUpdate = new Figure[GameImpl.ROWS * GameImpl.COLS];
		
		for(int i=0; i<(GameImpl.ROWS * GameImpl.COLS - 2 * GameImpl.COLS); i++){
			preAssignment[i] = null;
			fieldForUpdate[i] = null;
		}
		for(int i=0; i<14; i++){
			preAssignment[28+i] = theAssignment[i];
			if(theAssignment[i] != null){
				fieldForUpdate[28+i] = new Figure(theAssignment[i], player);
			}
		}
		
		uiController.updateBoard(fieldForUpdate);
	}
	
	private void checkAssignment(FigureKind[] assignment){
		if(assignment.length != 42){
			throw new RuntimeException("The preAssignment in gameController must contain 42 FigureKinds.");
		}
		for(int i=0; i<assignment.length; i++){
			if((i < 28 && assignment[i] != null) || (i >= 28 && assignment[i] == null)){
				throw new RuntimeException("The preAssigmnent in gameController must have null values in the first 28 fields and figureKinds in fields 28 to 41.");
			}
		}
	}
	
	/**
	 * Resets the initial Assignment.
	 */
	public void resetAssignment(){
		for(int i=0; i<14; i++){
			preAssignment[i]=null;			
		}
		uiController.updateBoard(new Figure[GameImpl.ROWS * GameImpl.COLS]);
	}
	
	public ArrayList<Integer> getPossibleMoves(int index) throws RemoteException{
		ArrayList<Integer> moves = new ArrayList<Integer>();
		Figure[] field = game.getField();
		
		if(field[index].getKind().equals(FigureKind.FLAG) ||
			field[index].getKind().equals(FigureKind.TRAP)){
			return moves;
		}
	
		// Move up
		if(getRow(index) > 0)
			if(field[index - 7] == null || !field[index - 7].belongsTo(player))
				moves.add(index - 7);

		// Move right
		if(getCol(index) < 6)
			if(field[index + 1] == null || !field[index + 1].belongsTo(player))
				moves.add(index + 1);
		
		// Move down
		if(getRow(index) < 5)
			if(field[index + 7] == null || !field[index + 7].belongsTo(player))
				moves.add(index + 7);
		
		// Move left
		if(getCol(index) > 0)
			if(field[index - 1] == null || !field[index - 1].belongsTo(player))
				moves.add(index - 1);
		
		return moves;
	}
	
	public int getRow(int index){
		return index/7;
	}
	
	public int getCol(int index){
		return index%7;
	}
}